package com.miaoyi.redisdemo.controller;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.miaoyi.redisdemo.constants.MqConstants;
import com.miaoyi.redisdemo.constants.PromotionConstants;
import com.miaoyi.redisdemo.domain.Coupon;
import com.miaoyi.redisdemo.domain.NetMall;
import com.miaoyi.redisdemo.dto.LoginFormDTO;
import com.miaoyi.redisdemo.dto.Result;
import com.miaoyi.redisdemo.dto.UserDTO;
import com.miaoyi.redisdemo.entity.Shop;
import com.miaoyi.redisdemo.entity.WxLotteryPrize;
import com.miaoyi.redisdemo.entity.WxLotteryPrizeRecord;
import com.miaoyi.redisdemo.service.IShopService;
import com.miaoyi.redisdemo.service.WxCommonUserService;
import com.miaoyi.redisdemo.service.WxLotteryPrizeRecordService;
import com.miaoyi.redisdemo.service.WxLotteryPrizeService;
import com.miaoyi.redisdemo.utils.*;
import constans.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.redisson.RedissonReadLock;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.miaoyi.redisdemo.utils.RedisConstants.*;

@RestController
@RequestMapping("/redisCombat")//路径统一用小写 这里无所谓 /redis/combat
@Api(value = "RedisCombatController", tags = "redis实战")
@Slf4j
public class RedisCombatController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private WxLotteryPrizeService lotteryPrizeService;
    @Autowired
    private IShopService shopService;
    @Autowired
    private WxLotteryPrizeRecordService lotteryPrizeRecordService;
    @Autowired
    private RedissonClient redissonClient;
    @Resource
    private CacheClient cacheClient;
    @Autowired
    private WxCommonUserService commonUserService;
    @Autowired
    private RabbitTemplate rabbitTemplate;


    /**
     * 传统的Session存储 分布式时不适用
     *
     * @return
     */
    @GetMapping("test1")
    @ApiOperation("发送验证码")
    public R test1(@RequestParam String phone) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合，返回错误信息
            return R.failed("手机号格式错误！");
        }
        // 3.符合，生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到 session 2分钟过期
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 5.发送验证码
        log.info("发送短信验证码成功，验证码：{}", code);
        // 返回ok
        return R.ok();
    }

    @PostMapping("/login")
    @ApiOperation("登录")
    public R login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合，返回错误信息
            return R.failed("手机号格式错误！");
        }
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {
            // 不一致，报错
            return R.failed("验证码错误");
        }
        //todo 这里需要查询数据库
        //todo 登录成功后 需要使用过滤器 存入到UserHolder ThreadLocal 中 具体看语雀redis实战笔记
        // 4.一致，根据手机号查询用户 select * from tb_user  where phone = ?
        //User user = query().eq("phone", phone).one();

        // 5.判断用户是否存在
       /* if (user == null) {
            // 6.不存在，创建新用户并保存
            user = createUserWithPhone(phone);
        }*/

        // 7.保存用户信息到 redis中
        // 7.1.随机生成token，作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        //UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        UserDTO userDTO = new UserDTO();
        userDTO.setNickName("huang");
        userDTO.setIcon("huang1");
        userDTO.setId(1L);


        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3.存储
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return R.ok(token);
    }

    @GetMapping("/{id}")
    @ApiOperation("缓存奖品")
    public R queryShopById(@PathVariable("id") Long id) {
        String key = RedisConstants.CACHE_PRIZE_KEY + id;
        String prize = stringRedisTemplate.opsForValue().get(key);

        if (StringUtils.isNotBlank(prize)) {
            //存在
            WxLotteryPrize wxLotteryPrize = JSONUtil.toBean(prize, WxLotteryPrize.class);
            return R.ok(wxLotteryPrize);
        }
        WxLotteryPrize lotteryPrize = lotteryPrizeService.getById(id);
        if (lotteryPrize == null) {
            return R.failed("奖品不存在！");
        }
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(lotteryPrize));
        // 7.返回
        return R.ok(lotteryPrize);
    }

    @GetMapping("/list")
    @ApiOperation("缓存所有奖品")
    public R queryShop() {
        String key = RedisConstants.CACHE_PRIZE_SOU_KEY;
        String prize = stringRedisTemplate.opsForValue().get(key);

        if (StringUtils.isNotBlank(prize)) {
            //存在
            List<WxLotteryPrize> wxLotteryPrizes = JSONUtil.toList(prize, WxLotteryPrize.class);
            return R.ok(wxLotteryPrizes);
        }
        List<WxLotteryPrize> prizes = lotteryPrizeService.list();

        if (CollUtil.isEmpty(prizes)) {
            return R.failed("奖品不存在！");
        }
        String s = JSONUtil.toJsonStr(prizes);
        stringRedisTemplate.opsForValue().set(key, s);
        // 7.返回
        return R.ok(prizes);
    }

    //todo 上面直缓存会出现数据不一致（重点） 以及 缓存击穿 缓存穿透问题
    // 要求数据比较强一致性  加上超时时间 加 主动更新缓存 （主动更新就是先更新数据库紧接着删除缓存 需要原子性操作）
    // 数据要求一致性低的  只需要加上超时时间（或者不加） +默认的内存淘汰即可
    // 要求超级强一致性的话 在加上超时时间 加 主动更新缓存 操作时得加锁

    @GetMapping("/solveGet")
    @ApiOperation("解决缓存数据不一致--查询")
    public R queryShopone(@RequestParam("id") Long id) {
        String key = RedisConstants.CACHE_PRIZE_KEY + id;
        String prize = stringRedisTemplate.opsForValue().get(key);

        if (StringUtils.isNotBlank(prize)) {
            //存在
            WxLotteryPrize wxLotteryPrize = JSONUtil.toBean(prize, WxLotteryPrize.class);
            return R.ok(wxLotteryPrize);
        }
        WxLotteryPrize lotteryPrize = lotteryPrizeService.getById(id);
        if (lotteryPrize == null) {
            return R.failed("奖品不存在！");
        }
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(lotteryPrize), 30L, TimeUnit.MINUTES);
        // 7.返回
        return R.ok(lotteryPrize);
    }

    @PostMapping("/solveupdate")
    @ApiOperation("解决缓存数据不一致-更新数据再删除缓存")
    public R solveupdate(@RequestBody WxLotteryPrize prize) {
        return lotteryPrizeService.solveupdate(prize);

    }

    //todo 解决缓存雪崩 过期时间 key设置均匀一点

    //todo 解决缓存穿透
    //方案 布隆过滤 、缓存空对象
    @PostMapping("/chuangtou")
    @ApiOperation("解决缓存穿透-缓存空对象")
    public R chuangtou(@RequestParam("id") Long id) {
        String key = RedisConstants.CACHE_PRIZE_KEY + id;
        String prize = stringRedisTemplate.opsForValue().get(key);

        if (StringUtils.isNotBlank(prize)) {
            //存在
            WxLotteryPrize wxLotteryPrize = JSONUtil.toBean(prize, WxLotteryPrize.class);
            return R.ok(wxLotteryPrize);
        }
        if ("".equals(prize)) {
            return R.failed("奖品不存在");
        }

        WxLotteryPrize lotteryPrize = lotteryPrizeService.getById(id);
        if (lotteryPrize == null) {
            //在此处缓存空对象
            stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
            return R.failed("奖品不存在！");
        }
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(lotteryPrize), 30L, TimeUnit.MINUTES);
        // 7.返回
        return R.ok(lotteryPrize);

    }


    //todo 解决缓存击穿
    //加互斥锁 逻辑过期

    @PostMapping("/jichuan")
    @ApiOperation("解决缓存穿透-缓存空对象")
    public R jichuan(@RequestParam("id") Long id) {
        String key = RedisConstants.CACHE_PRIZE_KEY + id;
        String prize = stringRedisTemplate.opsForValue().get(key);

        if (StringUtils.isNotBlank(prize)) {
            //存在
            WxLotteryPrize wxLotteryPrize = JSONUtil.toBean(prize, WxLotteryPrize.class);
            return R.ok(wxLotteryPrize);
        }
        if ("".equals(prize)) {
            return R.failed("奖品不存在");
        }

        WxLotteryPrize lotteryPrize = lotteryPrizeService.getById(id);
        if (lotteryPrize == null) {
            //在此处缓存空对象
            stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
            return R.failed("奖品不存在！");
        }
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(lotteryPrize), 30L, TimeUnit.MINUTES);
        // 7.返回
        return R.ok(lotteryPrize);

    }


    @PostMapping("/jichuansuo")
    @ApiOperation("解决缓存击穿 以及缓存穿透-使用锁")
    public R suo(@RequestParam("id") Long id) {
        //解决缓存击穿思路就是 查询不到缓存 去查询数据库构建缓存时加锁去构建缓存
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return R.failed("店铺不存在");
        }
        return R.ok(shop);
    }

    @PostMapping("/jichuanshij")
    @ApiOperation("解决缓存击穿 以及缓存穿透-使用时间")
    public R jichuanshij(@RequestParam("id") Long id) {

        //解决缓存击穿思路就是 查询到的缓存去判断是否过期 过期再去加锁就行重建 建议用线程池
        Shop shop = queryWithLogicalExpire(id);
        if (shop == null) {
            return R.failed("店铺不存在");
        }
        return R.ok(shop);
    }

    @PostMapping("/jt")
    @ApiOperation("使用工具解决缓存穿透 缓存击穿")
    public R jt(@RequestParam("id") Long id) {
        // 解决缓存穿透

        Shop shop = cacheClient
                .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 互斥锁解决缓存击穿
        // Shop shop = cacheClient
        //         .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 逻辑过期解决缓存击穿
        // Shop shop = cacheClient
        //         .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);

        if (shop == null) {
            return R.failed("店铺不存在！");
        }
        // 7.返回
        return R.ok(shop);

    }

    @PostMapping("/chao")
    @ApiOperation("解决超卖问题")
    public R chao(Long id) {
        //可以使用乐观锁 或者悲观锁
        //此使用只能适用于单体项目

        WxLotteryPrize prize = lotteryPrizeService.getById(id);
        if (prize == null) {
            return R.failed("商品不存在");
        }
        boolean success = lotteryPrizeService.update(Wrappers.lambdaUpdate(WxLotteryPrize.class)
                .eq(WxLotteryPrize::getId, id).gt(WxLotteryPrize::getPrizeNum, 0)
                .setSql("prize_num= prize_num -1"));
        if (!success) {
            // 扣减失败
            log.error("库存不足！");
            return R.failed("库存不足");
        }
        log.info("减少库存！");
        // 7.创建订单
        WxLotteryPrizeRecord record = new WxLotteryPrizeRecord();
        record.setPhone("135364");
        record.setPrizeName(prize.getPrizeName());
        lotteryPrizeRecordService.save(record);
        return R.ok();
    }


    @PostMapping("/yiren")
    @ApiOperation("一人一单")
    public R yiren(Long id) {
        //String phone = "135364";
        List<String> list = Stream.of("135364", "135365", "135366", "135367").collect(Collectors.toList());
        Collections.shuffle(list);
        String phone = list.get(0);//模拟多个人
        //此使用只能适用于单体项目
        //synchronized(userId.toString().intern()){ }
        WxLotteryPrize prize = lotteryPrizeService.getById(id);
        if (prize == null) {
            return R.failed("商品不存在");
        }
        synchronized (phone.intern()) {

            int count = lotteryPrizeRecordService.count(Wrappers.lambdaQuery(WxLotteryPrizeRecord.class)
                    .eq(WxLotteryPrizeRecord::getPhone, phone)
                    .eq(WxLotteryPrizeRecord::getPrizeName, prize.getPrizeName()));
            if (count > 0) {
                return R.failed("已经购买");
            }
            boolean success = lotteryPrizeService.update(Wrappers.lambdaUpdate(WxLotteryPrize.class)
                    .eq(WxLotteryPrize::getId, id).gt(WxLotteryPrize::getPrizeNum, 0)
                    .setSql("prize_num= prize_num -1"));
            if (!success) {
                // 扣减失败
                log.error("库存不足！");
                return R.failed("库存不足");
            }
            log.info("减少库存！");
            // 7.创建订单
           /* try {
                java.lang.Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            WxLotteryPrizeRecord record = new WxLotteryPrizeRecord();
            record.setPhone(phone);
            record.setPrizeName(prize.getPrizeName());
            record.setCreateTime(new Date());
            lotteryPrizeRecordService.save(record);
        }
        return R.ok();
    }

    @PostMapping("shiwu")
    @ApiOperation("事物导致锁失效")
    public R shiwu() {
        return lotteryPrizeService.shiwu();
    }

    @PostMapping("chushi")
    @ApiOperation("初始化数据到redis")
    public R chushihua() {
        // 1.组织数据
        WxLotteryPrize prize = lotteryPrizeService.getById(1);
        Map<String, String> map = new HashMap<>(4);
        //map.put("issueBeginTime", String.valueOf(System.currentTimeMillis()));
        //map.put("issueEndTime", String.valueOf(LocalDateTime.now().plusDays(1).getSecond()));
        map.put("prizeNum", prize.getPrizeNum().toString());
        map.put("prizeName", prize.getPrizeName());
        // 2.写缓存
        stringRedisTemplate.opsForHash().putAll(PromotionConstants.COUPON_CACHE_KEY_PREFIX + prize.getId(), map);
        return R.ok();
    }


    @PostMapping("/youhua")
    @ApiOperation("优化为redis")
    public R youhua() {
        log.info("进来1111");
        String userId = "1";
        Long prizeId = 1L;
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + prizeId;
        // 2.查询
        synchronized (prizeId.toString().intern()) {//锁奖品id
            Map<Object, Object> objMap = stringRedisTemplate.opsForHash().entries(key);
            if (objMap.isEmpty()) {
                return R.failed("商品不存在");
            }
            // 3.数据反序列化
            WxLotteryPrize prize = BeanUtils.mapToBean(objMap, WxLotteryPrize.class, false, CopyOptions.create());
            // 3.校验库存
            if (prize.getPrizeNum() <= 0) {
                return R.failed("库存不足");
            }
            // 4.校验每人限领数量
            // 4.1.查询领取数量
            String key2 = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + prizeId;
            Long count = stringRedisTemplate.opsForHash().increment(key2, userId, 1);
            // 4.2.校验限领数量
            if (count > 1) {
                log.info("超出领取数量");
                return R.failed("超出领取数量");
            }
            // 5.扣减优惠券库存
            stringRedisTemplate.opsForHash().increment(
                    PromotionConstants.COUPON_CACHE_KEY_PREFIX + prizeId, "prizeNum", -1);

            //这里可以修改为mq
            rabbitTemplate.convertAndSend(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, prize);
          /*  WxLotteryPrizeRecord record = new WxLotteryPrizeRecord();
            record.setPhone("13536477084");
            record.setPrizeName(prize.getPrizeName());
            record.setCreateTime(new Date());
            lotteryPrizeRecordService.save(record);*/
        }
        return R.ok();
    }


    //测试D:\javaStudy\app\nginx-1.12.0 启动nginx
    @PostMapping("/fbus")
    @ApiOperation("分布式锁解决一人一单超卖问题")
    public R fbus(Long id) {
        //此使用只能适用于单体项目
        WxLotteryPrize prize = lotteryPrizeService.getById(id);
        if (prize == null) {
            return R.failed("商品不存在");
        }
        //String phone = "135364";
        List<String> list = Stream.of("135364", "135365", "135366", "135367").collect(Collectors.toList());
        Collections.shuffle(list);
        String phone = list.get(0);//模拟多个人
        log.info("进来！");
        RLock lock = redissonClient.getLock(LOCK_MISSION_ITEM_KEY + phone);
        try {
            //获取锁(可重入)，指定锁的名称
            //lock.tryLock();//拿不到锁就算了
            //lock.tryLock(5,10,TimeUnit.SECONDS);//5秒尝试获取锁 获取不到就算了 拿到锁10秒后业务还在就把锁释放了
            //lock.lock();//一直阻塞 默认30秒
            lock.lock(5, TimeUnit.SECONDS);//一直阻塞 拿到锁5秒后业务还在就把锁释放了
            int count = lotteryPrizeRecordService.count(Wrappers.lambdaQuery(WxLotteryPrizeRecord.class)
                    .eq(WxLotteryPrizeRecord::getPhone, phone)
                    .eq(WxLotteryPrizeRecord::getPrizeName, prize.getPrizeName()));
            if (count > 0) {
                return R.failed("已经购买");
            }
            boolean success = lotteryPrizeService.update(Wrappers.lambdaUpdate(WxLotteryPrize.class)
                    .eq(WxLotteryPrize::getId, id).gt(WxLotteryPrize::getPrizeNum, 0)
                    .setSql("prize_num= prize_num -1"));
            if (!success) {
                // 扣减失败
                log.error("库存不足！");
                return R.failed("库存不足");
            }
            log.info("减少库存！");
            // 7.创建订单
           /* try {
                java.lang.Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            WxLotteryPrizeRecord record = new WxLotteryPrizeRecord();
            record.setPhone(phone);
            record.setPrizeName(prize.getPrizeName());
            record.setCreateTime(new Date());
            lotteryPrizeRecordService.save(record);
        } catch (Exception e) {
            log.error("锁失败", e);
        } finally {
            lock.unlock();
        }
        return R.ok();
    }

    //测试D:\javaStudy\app\nginx-1.12.0 启动nginx
    @GetMapping("/ces")
    @ApiOperation("测试分布式锁")
    public R fbus() {
        RLock lock = redissonClient.getLock(LOCK_MISSION_ITEM_KEY);
        boolean isLock = false;

        try {
            //lock.tryLock();//拿不到锁就算了
            //lock.tryLock(5,10,TimeUnit.SECONDS);//5秒尝试获取锁 获取不到就算了 拿到锁10秒后业务还在就把锁释放了
            lock.lock();//一直阻塞 默认30/10秒  3分之一
            //lock.lock(5, TimeUnit.SECONDS);//一直阻塞 拿到锁5秒后业务还在就把锁释放了
            // boolean b = lock.tryLock(3, 10, TimeUnit.SECONDS);
            //isLock= lock.tryLock(3, 10, TimeUnit.SECONDS);

            if (isLock) {
                System.out.println("拿到锁");
            } else {
                System.out.println("拿不到锁");
            }
        } catch (Exception e) {
            log.error("锁失败", e);
        } finally {
           /* if (isLock) {//try需要加这一步 lock.lock的不需要
                lock.unlock();
            }*/
            lock.unlock();
        }
        return R.ok();
    }

    //集群分布式锁可能会出现分布式锁失效 主节点宕机 那么主节点和从节点一起获取锁 那么就出现两条线程
    @PostMapping("/jicun")
    @ApiOperation("集群分布式锁")
    public R jicun() {
        //redissonClient 代表每个redis
        RLock lock1 = redissonClient.getLock(LOCK_MISSION_ITEM_KEY);
        RLock lock2 = redissonClient.getLock(LOCK_MISSION_ITEM_KEY);
        RLock lock3 = redissonClient.getLock(LOCK_MISSION_ITEM_KEY);
        RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);

        try {
            lock.lock();
            log.info("执行业务");
        } catch (Exception e) {
            log.error("锁失败", e);
        } finally {
            lock.unlock();
        }
        return R.ok();
    }


    //todo 秒杀业务修改 判断一人一单 判断库存数量 把下单业务放在mq去执行


    //达人探店
    //todo set zset都是集合


    @PostMapping("/dianz")
    @ApiOperation("点赞-再次点击取消")
    public R dianz(Long id) {
        //操作数据库 这种方式会导致一个用户无限点赞，明显是不合理的

        //为什么采用set集合 因为我们的数据是不能重复的

        //设计 set key :value、value.....

        // 1.获取登录用户
        //Long userId = UserHolder.getUser().getId();

        //这里用手机当作用户
        String phone = "13536477084";

        // 2.判断当前登录用户是否已经点赞
        String key = BLOG_LIKED_KEY + id;
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, phone);
        if (BooleanUtil.isFalse(isMember)) {
            //3.如果未点赞，可以点赞
            //3.1 数据库点赞数+1
            boolean isSuccess = lotteryPrizeService.update().setSql("prize_total = prize_total + 1").eq("id", id).update();
            //3.2 保存用户到Redis的set集合
            if (isSuccess) {
                stringRedisTemplate.opsForSet().add(key, phone);
            }
        } else {
            //4.如果已点赞，取消点赞
            //4.1 数据库点赞数-1
            boolean isSuccess = lotteryPrizeService.update().setSql("prize_total = prize_total - 1").eq("id", id).update();
            //4.2 把用户从Redis的set集合移除
            if (isSuccess) {
                stringRedisTemplate.opsForSet().remove(key, phone);
            }
        }

        return R.ok();
    }


    @PostMapping("/dianc")
    @ApiOperation("获取点赞次数")
    public R dianc(Long id) {
        String key = BLOG_LIKED_KEY + id;
        int size = stringRedisTemplate.opsForSet().members(key).size();
        return R.ok(size);
    }

    @PostMapping("/dianp")
    @ApiOperation("点赞排行榜-需要排行榜--存入的是用户 到时候值获取是用户")
    public R dianp(Long id) {
        //比如最早点赞的TOP5，形成点赞排行榜  采用一个可以排序的set集合，就是咱们的sortedSet
        // 1.获取登录用户
        //Long userId = UserHolder.getUser().getId();
        String phone = "13536477084";
        // 2.判断当前登录用户是否已经点赞
        String key = BLOG_LIKED_KEY + id;
        Double score = stringRedisTemplate.opsForZSet().score(key, phone);
        if (score == null) {
            // 3.如果未点赞，可以点赞
            // 3.1.数据库点赞数 + 1
            boolean isSuccess = lotteryPrizeService.update().setSql("prize_total = prize_total + 1").eq("id", id).update();
            // 3.2.保存用户到Redis的set集合  zadd key value score
            if (isSuccess) {
                stringRedisTemplate.opsForZSet().add(key, phone, System.currentTimeMillis());
            }
        } else {
            // 4.如果已点赞，取消点赞 如果不是可以取消的 执行return
            // 4.1.数据库点赞数 -1
            boolean isSuccess = lotteryPrizeService.update().setSql("prize_total = prize_total - 1").eq("id", id).update();
            // 4.2.把用户从Redis的set集合移除
            if (isSuccess) {
                stringRedisTemplate.opsForZSet().remove(key, phone);
            }
        }
        return R.ok();
    }


    @GetMapping("/likes/{id}")
    @ApiOperation("点赞排行榜-列表查询")
    public Result queryBlogLikes(@PathVariable("id") Long id) {
        String key = BLOG_LIKED_KEY + id;
        // 1.查询top5的点赞用户 zrange key 0 4
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if (top5 == null || top5.isEmpty()) {
            return Result.ok(Collections.emptyList());
        }
        // 2.解析出其中的用户id
        //List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
        List<String> phones = top5.stream().map(String::valueOf).collect(Collectors.toList());

        String idStr = StrUtil.join(",", phones);
        // 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
        List<UserDTO> userDTOS = commonUserService.query()
                .in("phone", phones).last("ORDER BY FIELD(id," + idStr + ")").list()
                .stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        // 4.返回
        return Result.ok(userDTOS);

        //代码获取到排行的id List<Long> ids 不能用listByIds方法 因为in(xx,xx) 不会按顺序来排序
        //所以得用ORDER BY FIELD(xx,xx)
    }

    @PostMapping("/dianzc")
    @ApiOperation("获取点赞的值")
    public R dianzc(Long id) {
        String key = BLOG_LIKED_KEY + id;
        Set<String> top = stringRedisTemplate.opsForZSet().range(key, 0, -1);
        return R.ok(top);
    }


    //在set集合中，有交集并集补集的api，我们可以把两人的关注的人分别放入到一个set集合中
    @PostMapping("/haoyou")
    @ApiOperation("好友关注-共同关注")
    public R haoyou(Long id) {
        // 1.获取当前用户

        //关注好友就存入set

        Long userId = UserHolder.getUser().getId();

        String phone = "13536477084";//当前用户

        String phone2 = "13536477085"; //前端传过来的那个用户

        String key = "follows:" + phone;
        // 2.求交集
        String key2 = "follows:" + phone2;
        //交集
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
        // union 并集
        if (intersect == null || intersect.isEmpty()) {
            // 无交集
            return R.ok(Collections.emptyList());
        }
        // 3.解析id集合
        List<String> phones = intersect.stream().map(String::valueOf).collect(Collectors.toList());
        // 4.查询用户
        List<UserDTO> users = commonUserService.query().in("phone", phones).list()
                .stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return R.ok(users);
    }


    @PostMapping("/sin")
    @ApiOperation("签到")
    public R sin() {
        // 1.获取当前登录用户
        //Long userId = UserHolder.getUser().getId();
        String userId = "111";
        // 2.获取日期
        LocalDateTime now = LocalDateTime.now();
        // 3.拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keySuffix;
        // 4.获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth();
        // 5.写入Redis SETBIT key offset 1
        stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
        return R.ok();
    }

    @PostMapping("/qdao")
    @ApiOperation("获取签到次数")
    public R qdao() {

        // 1.获取当前登录用户
        //Long userId = UserHolder.getUser().getId();
        String userId = "111";
        // 2.获取日期
        LocalDateTime now = LocalDateTime.now();
        // 3.拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keySuffix;
        // 4.获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth();
        // 5.获取本月截止今天为止的所有的签到记录，返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0
        List<Long> result = stringRedisTemplate.opsForValue().bitField(
                key,
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
        );
        if (result == null || result.isEmpty()) {
            // 没有任何签到结果
            return R.ok(0);
        }
        Long num = result.get(0);
        if (num == null || num == 0) {
            return R.ok(0);
        }
        // 6.循环遍历
        int count = 0;
        while (true) {
            // 6.1.让这个数字与1做与运算，得到数字的最后一个bit位  // 判断这个bit位是否为0
            if ((num & 1) == 0) {
                // 如果为0，说明未签到，结束
                break;
            } else {
                // 如果不为0，说明已签到，计数器+1
                count++;
            }
            // 把数字右移一位，抛弃最后一个bit位，继续下一个bit位
            num >>>= 1;
        }
        return R.ok(count);
    }


    @PostMapping("/pv")
    @ApiOperation("pv-uv")
    public R pvu(Long id) {
        // UV：全称Unique Visitor，也叫独立访客量，是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站，只记录1次。
        // PV：全称Page View，也叫页面访问量或点击量，用户每访问网站的一个页面，记录1次PV，用户多次打开页面，则记录多次PV。往往用来衡量网站的流量。
        String phone = "123";

        stringRedisTemplate.opsForHyperLogLog().add("uv", phone);
        stringRedisTemplate.opsForHyperLogLog().add("uv", phone);
        Long uv = stringRedisTemplate.opsForHyperLogLog().size("uv");


        stringRedisTemplate.opsForValue().increment("pv", 1);
        String pv = stringRedisTemplate.opsForValue().get("pv");
        HashMap<String, Object> map = new HashMap<>();
        map.put("pv", pv);
        map.put("uv", uv);
        return R.ok(map);
    }

    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public Shop queryWithLogicalExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2、判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 存在,直接返回
            // 4.命中，需要先把json反序列化为对象
            RedisData redisData = JSONUtil.toBean(json, RedisData.class);
            Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
            LocalDateTime expireTime = redisData.getExpireTime();
            // 5.判断是否过期
            if (expireTime.isAfter(LocalDateTime.now())) {
                // 5.1.未过期，直接返回店铺信息
                return shop;
            }
        }

        // 5.2.已过期，需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock) {
            //这里需要多一步再次检查key是否过期 做DoubleCheck 如果存在则无需重建缓存 高并发请求下可能多次构建 已做
            // 4.命中，需要先把json反序列化为对象
            if (StrUtil.isNotBlank(json)) {
                RedisData redisData = JSONUtil.toBean(json, RedisData.class);
                Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
                LocalDateTime expireTime = redisData.getExpireTime();
                // 5.判断是否过期
                if (expireTime.isAfter(LocalDateTime.now())) {
                    // 5.1.未过期，直接返回店铺信息
                    return shop;
                }
            }

            CACHE_REBUILD_EXECUTOR.submit(() -> {

                try {
                    //重建缓存
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        String jsonNew = stringRedisTemplate.opsForValue().get(key);
        RedisData redisData = JSONUtil.toBean(jsonNew, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        return shop;
    }

    private void saveShop2Redis(Long id, Long time) {
        Shop shop = shopService.getById(id);
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(time));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }


    public Shop getById(Serializable id) {
        return shopService.getById(id);
    }


    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }


    //解决缓存击穿 以及缓存穿透
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1、从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2、判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的值是否是空值 这里改成 "".equels(shopJson)  未命中(查询不到数据库)存的是 ""
        if ("".equals(shopJson)) {
            //返回一个错误信息
            return null;
        }
        // 4.实现缓存重构
        //4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2 判断否获取成功
            if (!isLock) {
                //4.3 失败，则休眠重试
                Thread.sleep(50);//这里获取不到锁 自旋再获取
                return queryWithMutex(id);
            }
            //4.4 成功，根据id查询数据库
            shop = shopService.getById(id);
            // 5.不存在，返回错误
            if (shop == null) {
                //将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //返回错误信息
                return null;
            }
            //6.写入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_NULL_TTL, TimeUnit.MINUTES);

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //7.释放互斥锁
            unlock(lockKey);
        }
        return shop;
    }
}
